<?php

/**
 * Model_Behavior_UCenter_User 实现基于 UCenter 的用户访问控制
 *
 * 该插件依赖 UCenterClient 软件包。
 */
class Model_Behavior_UCenter_User extends QDB_ActiveRecord_Behavior_Abstract
{
    /**
     * 插件的设置信息
     *
     * @var array
     */
    protected $_settings = array
    (
        // 指示用什么属性保存的用户名
        'username_prop'     => 'username',
        // 指示用什么属性保存用户的密码
        'password_prop'     => 'password',
        // 指示用什么属性保存用户的电子邮件
        'email_prop'        => 'email',

        // 是否使用用户角色
        'roles_enabled'         => false,
        // 角色对象的类名称
        'role_class'            => 'Role',
        // 用户对象用什么属性名来关联用户角色
        'userobj_roles_prop'    => 'roles',
        // 角色对象中，用什么属性保存角色名称
        'roleobj_name_prop'     => 'name',
        // 默认角色的名称
        'default_role_name'     => 'MEMBER',
        
        /**
         * 当用 aclData() 方法获得用户及角色信息时，要包含的用户对象属性名（用 , 分割多个）
         *
         * 例如当前用户对象的属性是：
         *
         * user_id:  1
         * username: ddoxt
         * email:    dsds@dsdsd.com
         *
         * 那么当 acldata_props 设置为 'username' 时，aclData() 返回的数组包含：
         *
         * user_id:  1
         * username: ddoxt
         *
         * 如果 acldata_props 设置为 'username, email'，则返回结果为：
         *
         * user_id:  1
         * username: ddoxt
         * email:    dsds@dsdsd.com
         *
         * 不管 acldata_props 中指定什么属性名，aclData() 返回结果都会包含名为 id 的键。
         */
        'acldata_props'     => 'username',
    );

    /**
     * UCenterClient
     *
     * @var UCenterClient
     */
    protected $_uc;

    /**
     * 绑定行为插件
     */
    function bind()
    {
        $this->_uc = UCenterClient::user();

        $this->_addEventHandler(self::BEFORE_CREATE, array($this, '_before_create'));
        $this->_addEventHandler(self::AFTER_DESTROY, array($this, '_after_destroy'));
        $this->_addEventHandler(self::AFTER_UPDATE,  array($this, '_after_update'));

        // 阻止对 username、password 属性的修改
        $this->_meta->update_reject[$this->_settings['username_prop']] = true;
        $this->_meta->update_reject[$this->_settings['password_prop']] = true;

        $this->_addStaticMethod('validateLogin',    array($this, 'validateLogin'));
        $this->_addStaticMethod('queryUser',        array($this, 'queryUser'));
        $this->_addStaticMethod('checkUsername',    array($this->_uc, 'checkUsername'));
        $this->_addStaticMethod('checkEmail',       array($this->_uc, 'checkEmail'));
        $this->_addStaticMethod('changePassword',   array($this, 'changePassword'));
        $this->_addStaticMethod('updatePassword',   array($this, 'updatePassword'));
        $this->_addStaticMethod('changeEmail',      array($this, 'changeEmail'));
        $this->_addStaticMethod('updateEmail',      array($this, 'updateEmail'));

        $this->_addDynamicMethod('changePassword',  array($this, 'changePasswordDyn'));
        $this->_addDynamicMethod('updatePassword',  array($this, 'updatePasswordDyn'));
        $this->_addDynamicMethod('changeEmail',     array($this, 'changeEmailDyn'));
        $this->_addDynamicMethod('updateEmail',     array($this, 'updateEmailDyn'));

        if ($this->_settings['roles_enabled'])
        {
            $this->_addDynamicMethod('aclData',      array($this, 'aclData'));
            $this->_addDynamicMethod('aclRoles',     array($this, 'aclRoles'));
        }
    }

    /**
     * 在 Model 保存前，调用 UCenter 进行用户注册，失败将阻止保存 Model
     *
     * @param QDB_ActiveRecord_Abstract $obj
     */
    function _before_create(QDB_ActiveRecord_Abstract $obj)
    {
        $username = $obj[$this->_settings['username_prop']];
        $password = $obj[$this->_settings['password_prop']];
        $email    = $obj[$this->_settings['email_prop']];
        $uid = $this->_uc->register($username, $password, $email);
        $obj->changePropForce($obj->idname(), $uid);
        // 如果接下来保存 Model 数据到数据库时失败，则拦截该异常，并删除刚刚注册的用户
        $this->_meta->addExceptionTrap($obj, self::CREATE_EXCEPTION, array($this, '_create_exception_handler'));
    }

    /**
     * 在 Model 保存失败抛出异常时，删除刚刚注册的用户
     *
     * @param QDB_ActiveRecord_Abstract $obj
     * @param Exception $ex
     */
    function _create_exception_handler(QDB_ActiveRecord_Abstract $obj, Exception $ex)
    {
        $username = $obj[$this->_settings['username_prop']];
        $this->_uc->delete($username);
    }

    /**
     * 通知 UCenter 删除指定用户
     *
     * @param QDB_ActiveRecord_Abstract $obj
     */
    function _after_destroy(QDB_ActiveRecord_Abstract $obj)
    {
        $username = $obj[$this->_settings['username_prop']];
        $this->_uc->delete($username);
    }

    /**
     * 通知 UCenter 更新用户信息
     *
     * @param QDB_ActiveRecord_Abstract $obj
     */
    function _after_update(QDB_ActiveRecord_Abstract $obj)
    {
        if ($obj->changed($this->_settings['email_prop']))
        {
            // 如果修改了 Email 属性，则通知 UCenter
            $this->_uc->edit($obj[$this->_settings['username_prop']], 
                             UCenterClient::NO_NEW_PASSWORD, 
                             UCenterClient::NO_NEW_EMAIL, 
                             $obj[$this->_settings['email_prop']], 
                             UCenterClient::IGNORE_PWD);
        }
    }

    /**
     * 验证用户登录并返回用户对象，失败抛出异常
     *
     * @param string $username
     * @param string $password
     * @param boolean $is_uid
     * @param string $source
     *
     * @return QDB_ActiveRecord_Abstract
     */
    function validateLogin($username, $password, $is_uid = UCenterClient::NOT_UID, $source = UCenterClient::UCENTER_DB)
    {
        $md5_password = md5($password);

        try
        {
            if ($source == UCenterClient::UCENTER_DB)
            {
                // 从 UCenter 读取用户信息
                list($uid, $username, $md5_password, $email) = $this->_uc->login($username, $password, $is_uid);
            }
            $user = $this->_meta->find(array($this->_settings['username_prop'] => $username))->query();

            if ($source == UCenterClient::LOCAL_DB)
            {
                // 从本地数据库读取用户信息
                $uid = $user->id();
                $email = $user[$this->_settings['email_prop']];
            }

            if (!$user->id())
            {
                // 如果本地用户信息读取失败
                if ($source == UCenterClient::LOCAL_DB)
                {
                    // 如果是本地验证失败，则尝试从 UCenter 获得用户信息，然后创建 Model
                    list($uid, $username, $email) = $this->_uc->get($username, $is_uid);
                }
                // 尝试创建缺少的本地用户
                $user = $this->_createMissingUser($uid, $username, $md5_password, $email);
            }
            elseif ($user->id() != $uid)
            {
                // UID 不一致的情况下，先创建用户，再删除已有的用户
                $new_user = $this->_createMissingUser($uid, $username, $md5_password, $email);
                $test = array(
                    $user->id() => true,
                    $this->_settings['username_prop'] => true, 
                    $this->_settings['password_prop'] => true, 
                    $this->_settings['email_prop'] => true
                );
                // 将已有对象的其他属性复制到新对象
                foreach ($user as $key => $value)
                {
                    if (isset($test[$key])) continue;
                    $new_user->changePropForce($key, $value);
                }
                $new_user->save();

                // 删除对象之前要禁用 AFTER_DESTROY 事件，否则会导致刚刚新建的对象就从 UCenter 中删除
                $this->_meta->removeEventHandler(self::AFTER_DESTROY, array($this, '_after_destroy'));
                try
                {
                    $user->destroy();
                }
                catch (Exception $ex)
                {
                    $this->_addEventHandler(self::AFTER_DESTROY, array($this, '_after_destroy'));
                    throw $ex;
                }
                $this->_addEventHandler(self::AFTER_DESTROY, array($this, '_after_destroy'));

                $user = $new_user;
            }
            elseif ($user[$this->_settings['username_prop']] != $username
                || $user[$this->_settings['password_prop']] != $md5_password
                || $user[$this->_settings['email_prop']] != $email)
            {
                // 如果基本信息不一致，则强制修改后保存
                $user->changePropForce($this->_settings['username_prop'], $username)
                     ->changePropForce($this->_settings['password_prop'], $md5_password)
                     ->changePropForce($this->_settings['email_prop'], $email)
                     ->save();
            }
        }
        catch (QException $ex)
        {
            return $this->_meta->newObject();
        }

        return $user;
    }

    /**
     * 创建本地数据库缺少的用户
     *
     * @param int $uid
     * @param string $username
     * @param string $md5_password
     * @param string $email
     * @param boolean $return_obj
     */
    protected function _createMissingUser($uid, $username, $md5_password, $email, $return_obj = true)
    {
        $this->_meta->removeEventHandler(self::BEFORE_CREATE, array($this, '_before_create'));

        try
        {
            $user = $this->_meta->newObject();

            if ($this->_settings['roles_enabled'])
            {
                $meta = QDB_ActiveRecord_Meta::instance($this->_settings['role_class']);
                $pn = $this->_settings['userobj_roles_prop'];
                // 尝试绑定默认角色
                $user[$this->_settings['userobj_roles_prop']] = array(
                    $meta->find(array($this->_settings['roleobj_name_prop'] => $this->_settings['default_role_name']))->query()
                );
            }

            $user->changePropForce($user->idname(), $uid)
                 ->changePropForce($this->_settings['username_prop'], $username)
                 ->changePropForce($this->_settings['password_prop'], $md5_password)
                 ->changePropForce($this->_settings['email_prop'], $email)
                 ->save(1, 'create');

            if (!$return_obj) return;

            // 重新查询刚刚创建的对象
            $user = $this->_meta->find(array($this->_settings['username_prop'] => $username))->query();
        }
        catch (Exception $ex)
        {
            $this->_meta->addEventHandler(self::BEFORE_CREATE, array($this, '_before_create'));
            throw $ex;
        }

        $this->_meta->addEventHandler(self::BEFORE_CREATE, array($this, '_before_create'));
        return $user;
    }

    /**
     * 查询指定用户名的对象，失败抛出异常
     *
     * @param string $username
     * @param boolean $is_uid
     
     * @return QDB_ActiveRecord_Abstract
     */
    function queryUser($username, $is_uid = UCenterClient::NOT_UID)
    {
        $arr = $this->_uc->get($username, $is_uid);
        if (!$arr)
        {
            throw new UCenterClient_NotExistsUsernameException($username);
        }

        list($uid, $username, $email) = $arr;
        $user = $this->_meta->find(array($this->_settings['username_prop'] => $username))->query();
        if ($user->id()) return $user;

        throw new UCenterClient_NotExistsLocalUserException($username);
    }

    /**
     * 修改用户的密码
     *
     * @param string $username
     * @param string $old_password
     * @param string $new_password
     */
    function changePassword($username, $old_password, $new_password)
    {
        $this->_uc->edit($username, $old_password, $new_password, UCenterClient::NO_NEW_EMAIL); 
    }

    /**
     * 直接更新用户密码，不需要原密码，失败抛出异常
     *
     * @param string $username
     * @param string $new_password
     * @param string $source
     */
    function updatePassword($username, $new_password, $source = UCenterClient::UCENTER_DB)
    {
        if ($source == UCenterClient::UCENTER_DB)
        {
            $this->_uc->edit($username, 
                             UCenterClient::NO_OLD_PASSWORD, 
                             $new_password, 
                             UCenterClient::NO_NEW_EMAIL, 
                             UCenterClient::IGNORE_PWD);
        }
        else
        {
            // 直接更新本地用户密码时需要检查本地用户是否存在
            $user = $this->_meta->find(array($this->_settings['username_prop'] => $username))->query();
            $md5_password = md5($new_password);
            if (!$user->id())
            {
                list($uid, $username, $email) = $this->_uc->get($username, $is_uid);
                $user = $this->_createMissingUser($uid, $username, $md5_password, $email, false);
            }
            else
            {
                $user->changePropForce($this->_settings['password_prop'], $md5_password);
                $user->save();
            }
        }
    }

    /**
     * 修改用户的电子邮件
     *
     * @param string $username
     * @param string $old_password
     * @param string $new_email
     */
    function changeEmail($username, $old_password, $new_email)
    {
        $this->_uc->edit($username, $old_password, UCenterClient::NO_NEW_PASSWORD, $new_email);
    }

    /**
     * 直接更新用户电子邮件，不需要原密码，失败抛出异常
     *
     * @param string $username
     * @param string $new_email
     */
    function updateEmail($username, $new_email)
    {
        $this->_uc->edit($username, 
                         UCenterClient::NO_OLD_PASSWORD, 
                         UCenterClient::NO_NEW_PASSWORD, 
                         $new_email);
    }

    /**
     * 修改用户密码，失败抛出异常
     *
     * @param QDB_ActiveRecord_Abstract $obj
     * @param string $old_password
     * @param string $new_password
     */
    function changePasswordDyn(QDB_ActiveRecord_Abstract $obj, $old_password, $new_password)
    {
        $this->_uc->edit($obj[$this->_settings['username_prop']], $old_password, $new_password, '');
    }

    /**
     * 直接更新用户密码，不需要原密码，失败抛出异常
     *
     * @param QDB_ActiveRecord_Abstract $obj
     * @param string $new_password
     * @param string $source
     */
    function updatePasswordDyn(QDB_ActiveRecord_Abstract $obj, $new_password, $source = UCenterClient::UCENTER_DB)
    {
        if ($source == UCenterClient::UCENTER_DB)
        {
            $this->_uc->edit($obj[$this->_settings['username_prop']], '', $new_password, '', true);
        }
        else
        {
            // 直接更新本地用户密码时需要检查本地用户是否存在
            $md5_password = md5($new_password);
            $user->changePropForce($this->_settings['password_prop'], $md5_password);
            $user->save();
        }
    }

    /**
     * 修改用户电子邮件，失败抛出异常
     *
     * @param QDB_ActiveRecord_Abstract $obj
     * @param string $old_password
     * @param string $new_email
     */
    function changeEmailDyn(QDB_ActiveRecord_Abstract $obj, $old_password, $new_email)
    {
        $this->_uc->edit($obj[$this->_settings['username_prop']], $old_password, '', $new_email);
    }

    /**
     * 直接更新用户电子邮件，不需要原密码，失败抛出异常
     *
     * @param QDB_ActiveRecord_Abstract $obj
     * @param string $new_email
     */
    function updateEmailDyn(QDB_ActiveRecord_Abstract $obj, $new_email)
    {
        $this->_uc->edit($obj[$this->_settings['username_prop']], '', '', $new_email, true);
    }

    /**
     * 获得用户的 ACL 数据
     *
     * @param QDB_ActiveRecord_Abstract $obj
     * @param string $acldata_props
     *
     * @return array
     */
    function aclData(QDB_ActiveRecord_Abstract $obj, $acldata_props = null)
    {
        if (is_null($acldata_props))
        {
            $acldata_props = $this->_settings['acldata_props'];
        }
        $acldata_props = Q::normalize($acldata_props);
        $data = array();
        foreach ($acldata_props as $pn)
        {
            $data[$pn] = $obj[$pn];
        }
        $data['id'] = $obj->id();
        return $data;
    }

    /**
     * 获得包含用户所有角色名的数组
     *
     * @param QDB_ActiveRecord_Abstract $obj
     *
     * @return array
     */
    function aclRoles(QDB_ActiveRecord_Abstract $obj)
    {
        $roles = array();
        foreach ($obj[$this->_settings['userobj_roles_prop']] as $role)
        {
            $roles[] = $role[$this->_settings['roleobj_name_prop']];
        }
        return $roles;
    }
    
}

/* @} */

